跳到主要内容

组件性能优化

影响网页性能最大的因素是浏览器的重绘(reflow)和 重排版(repaint)。React 背后的 Virtual DOM 就是尽可能地减少浏览器的重绘与重排版。

PureRender

官方在早期就为开发者提供了名为 react-addons-pure-render-mixin 的插件。其原理为重新实现了 shouldComponentUpdate 生命周期方法,让当前传入的 props 和 state 与之前的作浅比较,如果返回 false,那么组件就不会执行 render 方法。

PureRender 对 object 只作了引用比较,并没有作值比较。对于实现来说,这是一个取 舍问题。PureRender 源代码中只对新旧 props 作了浅比较。

import React, { Component } from 'react';
import PureRenderMixin from 'react-addons-pure-render-mixin';
class App extends Component {
constructor(props) {
super(props);
this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
}
render() {
return <div className={this.props.className}>foo</div>;
}
}

也可以用 pure-render-decorator 库实现了所需要的功能:

import pureRender from 'pure-render-decorator';
// es7才支持装饰器,这边需要配置babel
@pureRender
class List extends React.Component {
render() {
const { list } = this.props;
return (
<>
{list.map(item => {
return (
<div key={item.id}>
<div>{item.code}</div>
</div>
);
})}
</>
);
}
}
export default List;

优化 PureRender:

  • 直接为 props 设置对象或数组
// 这样设置 prop,则每次渲染时 style 都是新对象:
<Account style={{ color: 'black' }} />
// 或
<Account style={this.props.style || {}} />

// 我们只需要将默认值保存成同一份引用,就可以避免这个问题:
const defaultStyle = {};
<Account style={this.props.style || defaultStyle} />

// 同样,像在 props 中为对象或数据计算新值会使 PureRender 无效:
<Item items={this.props.items.filter(item => item.val > 30)} />
// 我们可以马上想到始终让对象或数组保持在内存中就可以增加命中率。但保持对象引用不符 合函数式编程的原则,这为函数带来了副作用,Immutable.js 可以优雅地解决这类问题。
  • 设置 props 方法并通过事件绑定在元素上
import React from 'react';

import React, { Component } from 'react';
// 该子组件接收 update 方法
class MyInput extends Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
}
handleChange(e) {
this.props.update(e.target.value);
}
render() {
return <input onChange={this.handleChange} />;
}
}

不用每次都绑定事件,因此把绑定移到构造器内。如果绑定方法需要传递参数,那么可 以考虑通过抽象子组件或改变现有数据结构解决。

  • 设置子组件

对于设置了子组件的 React 组件,在调用 shouldComponentUpdate 时,均返回 true。

import React, { Component } from 'react';
class NameItem extends Component {
render() {
// Item 组件 children 属性渲染 span 标签
return (<Item><span>Arcthur</span><Item/>)
}
}

上面的子组件 JSX 部分翻译过来,其实是:

<Item children={React.createElement('span', {}, 'Arcthur')}/>

显然,Item 组件不论什么情况下都会重新渲染。很简单,我们给 NameItem 设置 PureRender,也就是说提到父级来判断:

import React, { Component } from 'react';
import PureRenderMixin from 'react-addons-pure-render-mixin';

class NameItem extends Component {
constructor(props) {
super(props);
// 通过在 NameItem 组件 shouldComponentUpdate 方法中判断
// 这样就可以把 Item 组件作为一个 普通html标签
this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
}
render() {
return (<Item><span>Arcthur</span> <Item/>)
}
}

Immutable

JavaScript 中的对象一般是可变的(mutable),因为使用了引用赋值,新的对象简单地引用了原始对象,改变新的对象将影响到原始对象。

Immutable Data 就是一旦创建,就不能再更改的数据。对 Immutable 对象进行修改、添加或删除操作,都会返回一个新的 Immutable 对象。Immutable 实现的原理是持久化的数据结构 (persistent data structure),也就是使用旧数据创建新数据时,要保证旧数据同时可用且不变。同时为了避免深拷贝把所有节点都复制一遍带来的性能损耗,Immutable 使用了结构共享(structural sharing),即如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其他节点则进行共享。

Immutable.js 库内部实现了一套完整的持久化数据结构,还有很多易用的数据类型,比如 Collection、List、Map、Set、Record、Seq。有非常全面的 map、filter、groupBy、reduce、find 等函数式操作方法。同时,API 也尽量与 JavaScript 的 Object 或 Array 类似。

其中有 3 种最重要的数据结构说明一下:

  • Map:键值对集合,对应于 Object,ES6 也有专门的 Map 对象。
  • List:有序可重复的列表,对应于 Array。
  • ArraySet:无序且不可重复的列表。

Immutable 的优点:

  • 降低了“可变”带来的复杂度。可变数据耦合了 time 和 value 的概念,造成了数据很难被回溯。
  • 节省内存。Immutable 使用结构共享尽量复用内存。没有被引用的对象会被垃圾回收:
const { Map } = require('immutable');
let a = Map({
select: 'users',
filter: Map({ name: 'Cam' }),
});
let b = a.set('select', 'people');
console.log(a == b); // false
// a 和 b 共享了没有变化的 filter 节点。
console.log(a.get('filter') === b.get('filter')); // true
  • 撤销/重做,复制/粘贴,甚至时间旅行这些功能做起来都是小菜一碟。
  • 并发安全。
  • 拥抱函数式编程。

使用 Immutable 的缺点:

  • 很难区分到底是 Immutable 对象还是原生对象。
  • Immutable 中的 Map 和 List 虽然对应的是 JavaScript 的 Object 和 Array,但操作完全不同
  • 当使用第三方库的时候,一般需要使用原生对象,同样容易忘记转换对象。
    • 使用 FlowType 或 TypeScript 静态类型检查工具;
    • 约定变量命名规则,如所有 Immutable 类型对象以 $$ 开头;
    • 使用 Immutable.fromJS 而不是 Immutable.Map 或 Immutable.List 来创建对象,这样可以避免 Immutable 对象和原生对象间的混用。

两个 Immutable 对象可以使用 === 来比较,这样是直接比较内存地址,其性能最好。但是 即使两个对象的值是一样的,也会返回 false:

let map1 = Immutable.Map({a:1, b:1, c:1});
let map2 = Immutable.Map({a:1, b:1, c:1});
map1 === map2; // => false

// 为了直接比较对象的值,Immutable 提供了 Immutable.is 来作“值比较”:

Immutable.is(map1, map2); // => true

Immutable.is 比较的是两个对象的 hashCode 或 valueOf(对于 JavaScript 对象)。由于 Immutable 内部使用了 trie 数据结构来存储,只要两个对象的 hashCode 相等,值就是一样的。 这样的算法避免了深度遍历比较,因此性能非常好。

Immutable 与 cursor

由于 Immutable 数据一般嵌套非常深,所以为了便于访问深层数据,cursor 提供了可以直接访问这个深层数据的引用:

const Immutable = require('immutable');
const Cursor = require('immutable/contrib/cursor');
let data = Immutable.fromJS({ a: { b: { c: 1 } } }); //让cursor指向{ c: 1 }
let cursor = Cursor.from(data, ['a', 'b'], newData => {
// 当 cursor 或其子 cursor 执行更新时调用
console.log(newData);
});
cursor.get('c'); // 1
cursor = cursor.update('c', x => x + 1);
cursor.get('c'); // 2

Immutable 与 PureRender

Immutable.js则提供了简洁、高效的判断数据是否变化的方法,只需 === 和 is 比较就能知 道是否需要执行 render,而这个操作几乎零成本,所以可以极大提高性能。修改后的 shouldComponentUpdate 是这样的:

import React, { Component } from 'react';
import { is } from 'immutable';

class App extends Component {
shouldComponentUpdate(nextProps, nextState) {
const thisProps = this.props || {};
const thisState = this.state || {};
if (Object.keys(thisProps).length !== Object.keys(nextProps).length || Object.keys(thisState).length !== Object.keys(nextState).length) {
return true;
}
for (const key in nextProps) {
if (nextProps.hasOwnProperty(key) && !is(thisProps[key], nextProps[key])) {
return true;
}
}
for (const key in nextState) {
if (nextState.hasOwnProperty(key) && !is(thisState[key], nextState[key])) {
return true;
}
}
return false;
}
}

Immutable 与 setState

React 建议把 this.state 当做不可变,因此修改前需要做一个深拷贝:

import React, { Component } from 'react';
import '_' from 'lodash';

class App extends Component {
constructor(props) {
super(props);
this.state = {
data: { times: 0 },
}
}
handleAdd () {
let data = _.cloneDeep(this.state.data);
// let data = this.state.data;
data.times = data.times + 1;
this.setState({ data: data });
// 如果上面不做 cloneDeep,下面打印的结果会是加 1 后的值 console.log(this.state.data.times);
}
}

但在使用 Immutable 后,操作变得很简单:

import React, { Component } from 'react';

class App extends Component {
constructor(props) {
super(props);
this.state = {
data: Map({ times: 0 }),
}
}
handleAdd () {
this.setState(({ data }) => ({
data: data.update('times', v => v + 1),
}));
// 这时的 times 并不会改变
console.log(this.state.data.get('times'));
}
}

Immutable 可以给应用带来极大的性能提升,但是否使用还要看项目情况。

key

对 key 有一个原则,那就是独一无二,且能不用遍历或随机值就不用,除非列表内容也并不是唯一的表示,且没有可以相匹配的属性。

关于 key,我们还需要知道的一种情况是,有两个子组件需要渲染的时候,我们没法给它们设 key。这时需要用到 React 插件 createFragment 来解决:

// React 16 之前
import React from 'react';
import createFragment from 'react-addons-create-fragment';

// createFragment 相当于 DOM 的 document.createDocumentFragment
function Rank ({ first, second }) { // first, second 表示 React 组件
const children = createFragment({
first: first,
second: second,
});
return (<ul>{children}</ul>);
}
// React 16 之后

render() {
return (
<React.Fragment>
<ChildA />
<ChildB />
<ChildC />
</React.Fragment>
)
}
// 或
render() {
return (
<>
<ChildA />
<ChildB />
<ChildC />
</>
)
}

性能检测工具react-addons-perf

省略